package com.gko.numberencoding;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Pattern;

import com.gko.encoder.PhoneNumberToStringEncoder;
import com.gko.encoder.StringEncoder;
import com.gko.file.FileOperations;
import com.gko.util.StringUtil;

public class PhoneNumberProcessor {

	private boolean debugMode = false;
	private List<String> dictionary;
	private String pathToDictionary;
	private String pathToInputFile;
	private PrintStream printStream = System.out;

	private static final String MSG_INPUT_FILE_MISSING = Messages.getString("NumberEncoder.InputFileMissing"); //$NON-NLS-1$
	
	public PhoneNumberProcessor(String pathToDictionary, String pathToInputFile,
			boolean debugModeEnabled) {
		this.pathToDictionary = pathToDictionary;
		this.pathToInputFile = pathToInputFile;
		this.debugMode = debugModeEnabled;

		init();
	}

	public PrintStream getPrintStream() {
		return printStream;
	}

	public void setPrintStream(PrintStream printStream) {
		this.printStream = printStream;
	}

	/**
	 * Encodes the phone numbers into strings based on a set of predefined rules
	 * and prints them to the given output stream
	 * 
	 * @author gheorghi.kozlovskii
	 * */
	public void process() {

		// Reading the input
		BufferedReader inputFileReader = null;
		Path path = Paths.get(pathToInputFile);

		try {
			inputFileReader = Files.newBufferedReader(path,
					StandardCharsets.UTF_8);

			String phoneNumber = ""; //$NON-NLS-1$

			while ((phoneNumber = inputFileReader.readLine()) != null) {
				processPhoneNumber(phoneNumber);
			}

		} catch (IOException e) {
			System.out.print(MSG_INPUT_FILE_MISSING);
		} finally {
			closeReader(inputFileReader);
		}
	}

	private void processPhoneNumber(String phoneNumber) {
		List<String> phrases = buildPhrasesForPhoneNumber(phoneNumber, dictionary);
		excludeIllegalPhrasesFromList(phrases);
		printPhrasesForPhoneNumberToOutputStream(phoneNumber, phrases);
	}

	private void closeReader(BufferedReader inputFileReader) {
		try {
			inputFileReader.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	private void init() {
		dictionary = loadDictionary(pathToDictionary);
		Collections.sort(dictionary, Collections.reverseOrder());
	}

	private List<String> loadDictionary(final String path) {
		List<String> dictionary = FileOperations.readSmallTextFile(path, true,
				Messages.getString("NumberEncoder.DictionaryFileMissing")); //$NON-NLS-1$
		return dictionary;
	}

	private void logDebug(String string) {
		if (debugMode) {
			System.out.println("[DEBUG] " + string); //$NON-NLS-1$
		}
	}

	protected void excludeIllegalPhrasesFromList(List<String> phrases) {

		logDebug("Unfiltered phrases: " + phrases); //$NON-NLS-1$

		if (phrases.size() == 0) {
			return;
		}

		int phraseMinLength = StringUtil.findMinLength(phrases);

		for (int i = 0; i < phraseMinLength; i++) {

			if (!StringUtil.hasAtLeastOneLetterAt(i, phrases)) {
				continue;
			}

			List<String> phrasesToBeRemoved = new ArrayList<String>();

			for (String phrase : phrases) {

				char character = phrase.charAt(i);
				if (Character.isDigit(character)) {
					phrasesToBeRemoved.add(phrase);
				}
			}

			phrases.removeAll(phrasesToBeRemoved);

			if (phrases.size() == 0) {
				break;
			}
		}
	}

	/**
	 * Prints the input line and all the phrase matches. If no phrase matches
	 * were found, nothing is printed at all.
	 * 
	 * @param line
	 * @param phrases
	 * */
	protected void printPhrasesForPhoneNumberToOutputStream(String line,
			List<String> phrases) {

		if (phrases != null) {

			for (String phrase : phrases) {
				this.printStream.println(line + ": " + phrase); //$NON-NLS-1$
			}
		}
	}

	/**
	 * Translates the valid input string into zero or more words, using the
	 * provided dictionary.
	 * 
	 * @param input
	 *            The string that is going to be translated.
	 * @param dictionary
	 *            The dictionary that is going to be used.
	 * @return <p>
	 *         One or more dictionary words that match the input.
	 *         </p>
	 *         <p>
	 *         <b>null</b> - when no matches in the dictionary were found.
	 *         </p>
	 * */
	private List<String> buildPhrasesForPhoneNumber(String input, List<String> dictionary) {

		List<String> words = null;

		// Validates the input
		if (Pattern.matches("[\\d-/]+", input)) { //$NON-NLS-1$

			logDebug("Phone number: " + input + " - valid"); //$NON-NLS-1$ //$NON-NLS-2$

			// Fetching the possible character combination
			List<char[]> mappingCombinations = new ArrayList<char[]>();

			String normalizedInput = new String(input.replaceAll("[^\\d]", "")); //$NON-NLS-1$ //$NON-NLS-2$

			for (int i = 0; i < normalizedInput.length(); i++) {

				char[] mappings = digitToChar(normalizedInput.charAt(i));
				mappingCombinations.add(mappings);
			}

			// Requests the list of words
			StringEncoder encoder = new PhoneNumberToStringEncoder(dictionary);
			words = encoder.encode(mappingCombinations);

		} else {

			if (debugMode) {
				System.out.println("Phone number: " + input + " - INVALID!"); //$NON-NLS-1$ //$NON-NLS-2$
			}
		}
		return words;
	}

	/**
	 * @return The char mappings for the input char.
	 * */
	private char[] digitToChar(char charAt) {

		char[] retVal = null;

		switch (charAt) {
		case '0':
			retVal = new char[] { 'e', '0' };
			break;

		case '1':
			retVal = new char[] { 'j', 'n', 'q', '1' };
			break;

		case '2':
			retVal = new char[] { 'r', 'w', 'x', '2' };
			break;

		case '3':
			retVal = new char[] { 'd', 's', 'y', '3' };
			break;

		case '4':
			retVal = new char[] { 'f', 't', '4' };
			break;

		case '5':
			retVal = new char[] { 'a', 'm', '5' };
			break;

		case '6':
			retVal = new char[] { 'c', 'i', 'v', '6' };
			break;

		case '7':
			retVal = new char[] { 'b', 'k', 'u', '7' };
			break;

		case '8':
			retVal = new char[] { 'l', 'o', 'p', '8' };
			break;

		case '9':
			retVal = new char[] { 'g', 'h', 'z', '9' };
			break;

		default:
			break;
		}

		return retVal;
	}

}
